// Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#include <fstream>
#include <iostream>
#include <sstream>
#include <string>

using std::cerr;
using std::endl;
using std::string;
using std::stringstream;

const int kStatefulPartition = 1;
const int kRootfsPartition = 3;
const int kOemPartition = 8;
const int kEfiPartition = 12;
// In recovery image, the release kernel is at partition 4
// But in real OS kernel, it is at partition 2
const int kRecoveryKernel = 4;
const int kRealKernel = 2;

const string kConfigPath = "recovery.conf";
const string kImagePath = "release_image.bin";
const string kZipImagePath = "release_image.bin.zip";
const string kConfigUrl =
    "https://dl.google.com/dl/edgedl/chromeos/recovery/recovery.conf";

void error_exit(const string& error_string)
{
  cerr << "ERROR:" << error_string << endl;
  exit(1);
}

string get_output_device(int partition)
{
  // Determine output device by hardware architecture(x86 or arm)
  char arch[10];
  FILE* fp = popen("crossystem arch", "r");
  fscanf(fp, "%5s", arch);
  pclose(fp);
  stringstream device_buffer;
  string arch_string(arch);
  if (arch_string.find("x86") != string::npos) {
    device_buffer << "/dev/sda";
    if (partition >= 0) {
      device_buffer << partition;
    }
  } else if (arch_string.find("arm") != string::npos) {
    device_buffer << "/dev/mmcblk0";
    if (partition >= 0) {
      device_buffer << "p" << partition;
    }
  } else {
    // Fail to detect architecture
    error_exit("Failed to auto detect architecture.");
  }
  return device_buffer.str();
}

string mount_partition(int partition,
                       const string& mount_option="")
{
  // Mount with the specified partition with option.
  string partition_name;
  char mount_template[] = "mount_XXXXXX";
  string mount_point(mkdtemp(mount_template));
  mount_point += "/";
  partition_name = get_output_device(partition);
  stringstream mount_command;
  mount_command << "mount " << mount_option << " " << partition_name <<
                   " " << mount_point;
  cerr << mount_command.str() << endl;
  if (system(mount_command.str().c_str()) != 0) {
    error_exit("Failed to mount " + partition_name + ".");
  }
  return mount_point;
}

void unmount_partition(const string& mount_point)
{
  // Unmount stateful partition
  if (system(("umount " + mount_point).c_str()) != 0) {
    error_exit("Failed to ummount " + mount_point + ".");
  };
  // Remove temp directory
  if (remove(mount_point.c_str()) != 0) {
    error_exit("Unable to remove temporary mount point.");
  }
}

bool find_key_value(const string& line, const string& key, string& value)
{
  if (line.find(key) == 0) {
    int equal_pos = key.length();
    if (line[equal_pos] == '=') {
      value = line.substr(equal_pos + 1);
      return true;
    }
  }
  return false;
}

void initialize_partition()
{
  // Initialize partition table
  string pmbr = "/root/.pmbr_code";
  // Check if pmbr exists
  if (access(pmbr.c_str(), R_OK) != 0) {
    error_exit("Missing " + pmbr +", please rebuild image.");
  }

  string device_name = get_output_device(-1);
  if (system((". /usr/sbin/write_gpt.sh &&"
              " write_base_table " + device_name + " " + pmbr).c_str()) != 0) {
    error_exit("Cannot write partition table.");
  }
  cerr << "Reloading partition table changes..."  << endl;
  sync();
  if (system(("partprobe " + device_name).c_str()) != 0) {
    error_exit("Cannot reload partition table.");
  }
  cerr << "Make a new stateful partition." << endl;
  string stateful_partition = get_output_device(kStatefulPartition);
  if (system(("mkfs.ext4 " + stateful_partition).c_str()) != 0) {
    error_exit("Cannot make stateful partition.");
  }
  cerr << "Done preparing disk." << endl;
}

string find_board()
{
  const string kLsbRelease = "/etc/lsb-release";
  const string kBoardString = "CHROMEOS_RELEASE_BOARD";
  string buffer, board;
  std::ifstream fin;
  fin.open(kLsbRelease.c_str());
  while(std::getline(fin, buffer)) {
    if (find_key_value(buffer, kBoardString, board)) {
      break;
    }
  }
  cerr << "Board: " << board << endl;
  // Can not find board, error
  if (board.empty()) {
    error_exit("Can not find board name, installation failed.");
  }
  return board;
}

// The network interface may not be ready, so let's try multiple times here
void download_config(const string& stateful_mount)
{
  int retry = 10;
  while (retry-- > 0) {
    if (system(("wget " + kConfigUrl +
                " -O " + stateful_mount + kConfigPath +
                " --no-check-certificate").c_str()) == 0) {
      // Succeed and retrun
      return;
    }
    cerr << "Connection failed, wait 3 seconds and retry(" << retry <<
            "times left).";
    sleep(3);
  }
  // After 10 tries, return fail
  error_exit("Fail downloading recovery image configuration.");
}

void download_image(const string& stateful_mount, const string& board)
{
  std::ifstream fin;
  string buffer, filename, download_link;

  // To avoid the confusion of x86-alex and x86-alex-he,
  // change board from board to board_
  string board_ = board + "_";

  fin.open((stateful_mount + kConfigPath).c_str());
  while(std::getline(fin, buffer)) {
    if (find_key_value(buffer, "url", filename)) {
      if (filename.find(board_) != string::npos) {
        download_link = filename;
        break;
      }
    }
  }

  // Can not find download url, error
  if (download_link.empty()) {
    error_exit("Can not find download url, installation failed.");
  }

  // Download the release image
  if (system(("wget " + download_link +
              " -O " + stateful_mount + kZipImagePath +
              " --no-check-certificate").c_str()) != 0) {
    error_exit("Fail downloading release image.");
  }

  // Unzip the release image.
  // To use gzip to decompress .zip file, we assume the zip archieve contain
  // the release image and no other files.
  // /tmp/image.bin.zip -> /tmp/image.zip
  if (system(("gzip -d -f -S .zip " +
              stateful_mount + kZipImagePath).c_str()) != 0) {
    error_exit("Fail to unzip image.");
  }
}

int get_image_offset(const int partition, const string& path)
{
  // Read the offset of a partition in an image by cgpt
  FILE* fp;
  int offset;
  stringstream command;

  command << "cgpt show -b -i " << partition << " " << path;
  fp = popen(command.str().c_str(), "r");
  fscanf(fp, "%d", &offset);
  pclose(fp);

  return offset;
}

int get_image_size(const int partition, const string& path)
{
  // Read the size (by number of 512 byte sectors) of a partition in an image
  // by cgpt
  FILE* fp;
  int size;
  stringstream command;

  command << "cgpt show -s -i " << partition << " " << path;
  fp = popen(command.str().c_str(), "r");
  fscanf(fp, "%d", &size);
  pclose(fp);

  return size;
}

void install_partition(const string& stateful_mount,
                       const int partition,
                       const int dest=-1)
{
  int sector_size = 512;
  int begin_sector;
  int num_sectors;
  int dest_partition = (dest == -1) ? partition : dest;
  string image_path = stateful_mount + kImagePath;

  // Read the partition information
  begin_sector = get_image_offset(partition, image_path);
  num_sectors = get_image_size(partition, image_path);

  // We want to enlarge sector size to speed up dd
  // Increase as much as possible up to 8M
  while(begin_sector % 2 == 0 && num_sectors % 2 == 0 &&
        sector_size < 8 * 1024 * 1024 && num_sectors > 0) {
    sector_size *= 2;
    begin_sector /= 2;
    num_sectors /=2 ;
  }

  string output_device;
  output_device = get_output_device(dest_partition);

  stringstream command;
  // Generate dd command, pv is used to show progress.
  command << "dd if=" << image_path << " bs=" << sector_size <<
             " skip=" << begin_sector << " count=" << num_sectors <<
             " | pv -ptreb -B 2M -s " << (uint64_t)sector_size * num_sectors <<
             " | dd of=" << output_device << " bs=" << sector_size <<
             " iflag=fullblock oflag=dsync";

  // Install to device by dd.
  cerr << command.str().c_str() << endl;
  if (system(command.str().c_str()) != 0) {
    error_exit("Unable to install partition.");
  }
}

void install_firmware_updater()
{
  // Extract firmware updater from rootfs
  const string updater_path = "/usr/sbin/chromeos-firmwareupdate";
  string rootfs_mount;
  char tmp_name[] = "fw_XXXXXX";
  string tmp_updater(mktemp(tmp_name));
  rootfs_mount = mount_partition(kRootfsPartition, "-o ro -t ext2");
  if (system(("cp " + rootfs_mount + updater_path +
              " " + tmp_updater).c_str()) != 0) {
    error_exit("Fail to copy firmware updater.");
  }
  unmount_partition(rootfs_mount);

  // Run firmware update script
  if (system(("sh " + tmp_updater +
             " --force --mode=recovery").c_str()) != 0) {
    error_exit("Fail to run firmware updater.");
  }

  if (remove(tmp_updater.c_str()) != 0) {
    error_exit("Fail to remove temp updater.");
  }
}

void install_all_partition(const string& stateful_mount)
{
  install_partition(stateful_mount, kRecoveryKernel, kRealKernel);
  install_partition(stateful_mount, kRootfsPartition);
  install_partition(stateful_mount, kOemPartition);
  install_partition(stateful_mount, kEfiPartition);
}

int main()
{
  string board, stateful_mount;

  initialize_partition();
  stateful_mount = mount_partition(kStatefulPartition);
  board = find_board();
  download_config(stateful_mount);
  download_image(stateful_mount, board);
  install_all_partition(stateful_mount);
  unmount_partition(stateful_mount);
  install_firmware_updater();
  return 0;
}
